import 'dart:typed_data';

import 'package:dbio_utils/generic_filehandle/generic_file_handle.dart';
import 'package:flutter/services.dart';

const UNCOMPRESSED_POSITION = 1;

class GziIndex {
  GenericFileHandle fileHandle;
  GziIndex({this.fileHandle});

  List index;

  int _readLongWithOverflow(ByteBuffer buf, [offset = 0, unsigned = true]) {
    double long = ByteData.view(buf, offset, 8).getFloat32(0);
    if (long > double.maxFinite || long < double.minPositive) throw Exception('integer overflow');

    return long.toInt();
  }

  Future<List> _getIndex() async {
    if (this.index == null) this.index = await this._readIndex();
    return this.index;
  }

  Future<List> _readIndex() async {
    var buf = Uint8List(8);
    buf = await this.fileHandle.read(0, 8, 0);
    int numEntries = this._readLongWithOverflow(buf.buffer, 0, true);
    if (numEntries <= 0)
      return [
        [0, 0]
      ];

    var entries = List(numEntries + 1);
    entries[0] = [0, 0];

    // TODO rewrite this to make an index-index that stays in memory
    var bufSize = 8 * 2 * numEntries;
    if (bufSize > double.maxFinite) throw Exception('integer overflow');
    buf = await this.fileHandle.read(0, bufSize, 8);
    for (var entryNumber = 0; entryNumber < numEntries; entryNumber += 1) {
      var compressedPosition = this._readLongWithOverflow(
        buf.buffer,
        entryNumber * 16,
      );
      var uncompressedPosition = this._readLongWithOverflow(
        buf.buffer,
        entryNumber * 16 + 8,
      );
      entries[entryNumber + 1] = [compressedPosition, uncompressedPosition];
    }
    return entries;
  }

  getLastBlock() async {
    var entries = await this._getIndex();
    if (entries == null || entries.length == 0) return null;
    return entries[entries.length - 1];
  }

  /// get an array of block records that must be read to execute
  /// the given virtual read operation
  ///
  /// @param {number} position uncompressed read position
  /// @param {number} length uncompressed read length
  /// @returns {Promise} for an array of block records, the
  /// last of which should *not* be included in the read
  getRelevantBlocksForRead(length, position) async {
    var endPosition = position + length;
    if (length == 0) return [];
    List entries = await this._getIndex();
    var relevant = [];

    // binary search to find the block that the
    // read starts in and extend forward from that
    var compare = (entry, nextEntry) {
      var uncompressedPosition = entry[UNCOMPRESSED_POSITION];
      var nextUncompressedPosition = nextEntry ? nextEntry[UNCOMPRESSED_POSITION] : double.infinity;
      // block overlaps read start
      if (uncompressedPosition <= position && nextUncompressedPosition > position) {
        return 0;
        // block is before read start
      }
      if (uncompressedPosition < position) {
        return -1;
      }
      // block is after read start
      return 1;
    };

    var lowerBound = 0;
    var upperBound = entries.length - 1;
    var searchPosition = (entries.length / 2).floor();

    var comparison = compare(
      entries[searchPosition],
      entries[searchPosition + 1],
    );
    while (comparison != 0) {
      if (comparison > 0) {
        upperBound = searchPosition - 1;
      } else if (comparison < 0) {
        lowerBound = searchPosition + 1;
      }
      searchPosition = ((upperBound - lowerBound) / 2).ceil() + lowerBound;
      comparison = compare(entries[searchPosition], entries[searchPosition + 1]);
    }

    // here's where we read forward
    relevant.add(entries[searchPosition]);
    var i = searchPosition + 1;
    for (; i < entries.length; i += 1) {
      relevant.add(entries[i]);
      if (entries[i][UNCOMPRESSED_POSITION] >= endPosition) break;
    }
    if (relevant[relevant.length - 1][UNCOMPRESSED_POSITION] < endPosition) {
      relevant.add([]);
    }
    return relevant;
  }
}
